/*
 * contrib/spi/refint.c
 *
 *
 * refint.c -- 一组使用通用触发器定义引用完整性约束的函数。
 */
#include "postgres.h"

#include <ctype.h>

#include "commands/trigger.h"
#include "executor/spi.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/rel.h"

PG_MODULE_MAGIC;

typedef struct
{
	char	   *ident;
	int			nplans;
	SPIPlanPtr *splan;
} EPlan;

static EPlan *FPlans = NULL;
static int	nFPlans = 0;
static EPlan *PPlans = NULL;
static int	nPPlans = 0;

static EPlan *fc_find_plan(char *fc_ident, EPlan **fc_eplan, int *fc_nplans);

/*
 * check_primary_key () -- 检查被插入/更新的元组中的键是否引用“主”表中现有的元组。
 * 虽然它是无参数调用的，但在创建触发器时必须指定引用的
 * 表/键：触发的表中的键字段名、引用的表名、引用的键字段名：
 * EXECUTE PROCEDURE
 * check_primary_key ('Fkey1', 'Fkey2', 'Ptable', 'Pkey1', 'Pkey2')。
 */

PG_FUNCTION_INFO_V1(check_primary_key);

Datum check_primary_key(PG_FUNCTION_ARGS)
{
	TriggerData *fc_trigdata = (TriggerData *) fcinfo->context;
	Trigger    *fc_trigger;		/* 获取触发器名称 */
	int			fc_nargs;			/* 在 CREATE TRIGGER 中指定的参数数量 */
	char	  **fc_args;			/* 参数：列名和表名 */
	int			fc_nkeys;			/* 键列数量 (= nargs / 2) */
	Datum	   *fc_kvals;			/* 键值 */
	char	   *fc_relname;		/* 被引用的关系名 */
	Relation	fc_rel;			/* 触发的关系 */
	HeapTuple	fc_tuple = NULL;	/* 要返回的元组 */
	TupleDesc	fc_tupdesc;		/* 元组描述 */
	EPlan	   *fc_plan;			/* 已准备的计划 */
	Oid		   *fc_argtypes = NULL;	/* 准备执行计划的键类型 */
	bool		fc_isnull;			/* 确定某列是否为 NULL */
	char		fc_ident[2 * NAMEDATALEN]; /* 识别我自己 */
	int			fc_ret;
	int			fc_i;

#ifdef	DEBUG_QUERY
	elog(DEBUG4, "check_primary_key: Enter Function");
#endif

	/*
	 * 首先进行一些检查...
	 */

	/* 是由触发器管理器调用吗？ */
	if (!CALLED_AS_TRIGGER(fcinfo))
		/* 内部错误 */
		elog(ERROR, "check_primary_key: not fired by trigger manager");

	/* 应该为 ROW 触发器调用 */
	if (!TRIGGER_FIRED_FOR_ROW(fc_trigdata->tg_event))
		/* 内部错误 */
		elog(ERROR, "check_primary_key: must be fired for row");

	/* 如果是插入，则必须检查要插入的元组 */
	if (TRIGGER_FIRED_BY_INSERT(fc_trigdata->tg_event))
		fc_tuple = fc_trigdata->tg_trigtuple;

	/* 不应该为删除调用 */
	else if (TRIGGER_FIRED_BY_DELETE(fc_trigdata->tg_event))
		/* 内部错误 */
		elog(ERROR, "check_primary_key: cannot process DELETE events");

	/* 如果是更新，则必须检查新元组，而不是旧元组 */
	else
		fc_tuple = fc_trigdata->tg_newtuple;

	fc_trigger = fc_trigdata->tg_trigger;
	fc_nargs = fc_trigger->tgnargs;
	fc_args = fc_trigger->tgargs;

	if (fc_nargs % 2 != 1)			/* 参数数量为奇数！ */
		/* 内部错误 */
		elog(ERROR, "check_primary_key: odd number of arguments should be specified");

	fc_nkeys = fc_nargs / 2;
	fc_relname = fc_args[fc_nkeys];
	fc_rel = fc_trigdata->tg_relation;
	fc_tupdesc = fc_rel->rd_att;

	/* 连接到 SPI 管理器 */
	if ((fc_ret = SPI_connect()) < 0)
		/* 内部错误 */
		elog(ERROR, "check_primary_key: SPI_connect returned %d", fc_ret);

	/*
	 * 我们使用 SPI 计划准备功能，因此分配空间以放置键
	 * 值。
	 */
	fc_kvals = (Datum *) palloc(fc_nkeys * sizeof(Datum));

	/*
	 * 构造标识字符串作为 TriggerName $ TriggeredRelationId 并尝试
	 * 查找准备好的执行计划。
	 */
	snprintf(fc_ident, sizeof(fc_ident), "%s$%u", fc_trigger->tgname, fc_rel->rd_id);
	fc_plan = fc_find_plan(fc_ident, &PPlans, &nPPlans);

	/* 如果没有计划，则分配 argtypes 以进行准备 */
	if (fc_plan->nplans <= 0)
		fc_argtypes = (Oid *) palloc(fc_nkeys * sizeof(Oid));

	/* 针对键中的每一列... */
	for (fc_i = 0; fc_i < fc_nkeys; fc_i++)
	{
		/* 获取元组中列的索引 */
		int			fc_fnumber = SPI_fnumber(fc_tupdesc, fc_args[fc_i]);

		/* 坏家伙可能在 CREATE TRIGGER 中给我们提供不存在的列 */
		if (fc_fnumber <= 0)
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_COLUMN),
					 errmsg("there is no attribute \"%s\" in relation \"%s\"",
							fc_args[fc_i], SPI_getrelname(fc_rel))));

		/* 好吧，获取列的二进制（内部格式）值 */
		fc_kvals[fc_i] = SPI_getbinval(fc_tuple, fc_tupdesc, fc_fnumber, &fc_isnull);

		/*
		 * 如果它是 NULL，则无事可做！不要忘记调用 SPI_finish ()！
		 * 不要忘记返回元组！执行器插入您要返回的元组！
		 * 如果您返回 NULL，则不会插入任何内容！
		 */
		if (fc_isnull)
		{
			SPI_finish();
			return PointerGetDatum(fc_tuple);
		}

		if (fc_plan->nplans <= 0)	/* 获取列的 typeId */
			fc_argtypes[fc_i] = SPI_gettypeid(fc_tupdesc, fc_fnumber);
	}

	/*
	 * 如果我们需要准备计划...
	 */
	if (fc_plan->nplans <= 0)
	{
		SPIPlanPtr	fc_pplan;
		char		fc_sql[8192];

		/*
		 * 构造查询：SELECT 1 FROM _referenced_relation_ WHERE Pkey1 =
		 * $1 [AND Pkey2 = $2 [...]]
		 */
		snprintf(fc_sql, sizeof(fc_sql), "select 1 from %s where ", fc_relname);
		for (fc_i = 0; fc_i < fc_nkeys; fc_i++)
		{
			snprintf(fc_sql + strlen(fc_sql), sizeof(fc_sql) - strlen(fc_sql), "%s = $%d %s",
					 fc_args[fc_i + fc_nkeys + 1], fc_i + 1, (fc_i < fc_nkeys - 1) ? "and " : "");
		}

		/* 为查询准备计划 */
		fc_pplan = SPI_prepare(fc_sql, fc_nkeys, fc_argtypes);
		if (fc_pplan == NULL)
			/* 内部错误 */
			elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result));

		/*
		 * 请记住，SPI_prepare 将计划放置在当前内存上下文中 -
		 * 因此，我们必须将计划保存在 TopMemoryContext 中以便后续使用。
		 */
		if (SPI_keepplan(fc_pplan))
			/* 内部错误 */
			elog(ERROR, "check_primary_key: SPI_keepplan failed");
		fc_plan->splan = (SPIPlanPtr *) MemoryContextAlloc(TopMemoryContext,
														sizeof(SPIPlanPtr));
		*(fc_plan->splan) = fc_pplan;
		fc_plan->nplans = 1;
	}

	/*
	 * 好的，执行已准备好的计划。
	 */
	fc_ret = SPI_execp(*(fc_plan->splan), fc_kvals, NULL, 1);
	/* 我们没有 NULL - 所以我们在这里传递 ^^^^ */

	if (fc_ret < 0)
		/* 内部错误 */
		elog(ERROR, "check_primary_key: SPI_execp returned %d", fc_ret);

	/*
	 * 如果 SELECT 返回的元组数量为零，则...
	 */
	if (SPI_processed == 0)
		ereport(ERROR,
				(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
				 errmsg("tuple references non-existent key"),
				 errdetail("Trigger \"%s\" found tuple referencing non-existent key in \"%s\".", fc_trigger->tgname, fc_relname)));

	SPI_finish();

	return PointerGetDatum(fc_tuple);
}

/*
 * check_foreign_key () -- 检查被删除/更新的元组中的键是否未被“外部”表中的元组引用。
 * 虽然它被调用时没有参数，但你在创建触发器时必须指定：
 * 引用的数量、如果键被引用时的操作
 * （'restrict' | 'setnull' | 'cascade'），触发表中的键字段名称和引用表/键：
 * EXECUTE PROCEDURE
 * check_foreign_key (2, 'restrict', 'Pkey1', 'Pkey2',
 * 'Ftable1', 'Fkey11', 'Fkey12', 'Ftable2', 'Fkey21', 'Fkey22').
 */

PG_FUNCTION_INFO_V1(check_foreign_key);

Datum check_foreign_key(PG_FUNCTION_ARGS)
{
	TriggerData *fc_trigdata = (TriggerData *) fcinfo->context;
	Trigger    *fc_trigger;		/* 获取触发器名称 */
	int			fc_nargs;			/* 在 CREATE TRIGGER 中指定的参数数量 */
	char	  **fc_args;			/* 参数：如上所述 */
	char	  **fc_args_temp;
	int			fc_nrefs;			/* 引用的数量（== 计划的数量） */
	char		fc_action;			/* 'R'estrict | 'S'etnull | 'C'ascade */
	int			fc_nkeys;			/* 键列的数量 */
	Datum	   *fc_kvals;			/* 键值 */
	char	   *fc_relname;		/* 引用关系名称 */
	Relation	fc_rel;			/* 触发的关系 */
	HeapTuple	fc_trigtuple = NULL;	/* 将要更改的元组 */
	HeapTuple	fc_newtuple = NULL;	/* 要返回的元组 */
	TupleDesc	fc_tupdesc;		/* 元组描述 */
	EPlan	   *fc_plan;			/* 准备的计划 */
	Oid		   *fc_argtypes = NULL;	/* 准备执行计划的键类型 */
	bool		fc_isnull;			/* 确定某列是否为 NULL */
	bool		fc_isequal = true; /* 在两个元组中键是否相等（在 UPDATE 中） */
	char		fc_ident[2 * NAMEDATALEN]; /* 识别我自己 */
	int			fc_is_update = 0;
	int			fc_ret;
	int			fc_i,
				fc_r;

#ifdef DEBUG_QUERY
	elog(DEBUG4, "check_foreign_key: Enter Function");
#endif

	/*
	 * 首先进行一些检查...
	 */

	/* 是由触发器管理器调用吗？ */
	if (!CALLED_AS_TRIGGER(fcinfo))
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: not fired by trigger manager");

	/* 应该为 ROW 触发器调用 */
	if (!TRIGGER_FIRED_FOR_ROW(fc_trigdata->tg_event))
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: must be fired for row");

	/* 不应该为 INSERT 调用 */
	if (TRIGGER_FIRED_BY_INSERT(fc_trigdata->tg_event))
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: cannot process INSERT events");

	/* 必须检查 tg_trigtuple - 被删除的元组 */
	fc_trigtuple = fc_trigdata->tg_trigtuple;

	/*
	 * 但如果这是 UPDATE，那么我们必须返回 tg_newtuple。此外，如果键
	 * 在 tg_newtuple 中和 tg_trigtuple 中相同，那么就不需要做任何事情。
	 */
	fc_is_update = 0;
	if (TRIGGER_FIRED_BY_UPDATE(fc_trigdata->tg_event))
	{
		fc_newtuple = fc_trigdata->tg_newtuple;
		fc_is_update = 1;
	}
	fc_trigger = fc_trigdata->tg_trigger;
	fc_nargs = fc_trigger->tgnargs;
	fc_args = fc_trigger->tgargs;

	if (fc_nargs < 5)				/* nrefs，操作，键，关系，键 - 至少 */
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: too short %d (< 5) list of arguments", fc_nargs);

	fc_nrefs = pg_strtoint32(fc_args[0]);
	if (fc_nrefs < 1)
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: %d (< 1) number of references specified", fc_nrefs);
	fc_action = tolower((unsigned char) *(fc_args[1]));
	if (fc_action != 'r' && fc_action != 'c' && fc_action != 's')
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: invalid action %s", fc_args[1]);
	fc_nargs -= 2;
	fc_args += 2;
	fc_nkeys = (fc_nargs - fc_nrefs) / (fc_nrefs + 1);
	if (fc_nkeys <= 0 || fc_nargs != (fc_nrefs + fc_nkeys * (fc_nrefs + 1)))
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: invalid number of arguments %d for %d references",
			 fc_nargs + 2, fc_nrefs);

	fc_rel = fc_trigdata->tg_relation;
	fc_tupdesc = fc_rel->rd_att;

	/* 连接到 SPI 管理器 */
	if ((fc_ret = SPI_connect()) < 0)
		/* 内部错误 */
		elog(ERROR, "check_foreign_key: SPI_connect returned %d", fc_ret);

	/*
	 * 我们使用 SPI 计划准备功能，因此分配空间以放置键
	 * 值。
	 */
	fc_kvals = (Datum *) palloc(fc_nkeys * sizeof(Datum));

	/*
	 * 构建标识字符串作为 TriggerName $ TriggeredRelationId 并尝试
	 * 查找准备好的执行计划。
	 */
	snprintf(fc_ident, sizeof(fc_ident), "%s$%u", fc_trigger->tgname, fc_rel->rd_id);
	fc_plan = fc_find_plan(fc_ident, &FPlans, &nFPlans);

	/* 如果没有计划，则分配用于准备的 argtypes */
	if (fc_plan->nplans <= 0)
		fc_argtypes = (Oid *) palloc(fc_nkeys * sizeof(Oid));

	/*
	 * 否则 - 检查我们是否准备了正好 nrefs 计划
	 */
	else if (fc_plan->nplans != fc_nrefs)
		/* 内部错误 */
		elog(ERROR, "%s: check_foreign_key: # of plans changed in meantime",
			 fc_trigger->tgname);

	/* 针对键中的每一列... */
	for (fc_i = 0; fc_i < fc_nkeys; fc_i++)
	{
		/* 获取元组中列的索引 */
		int			fc_fnumber = SPI_fnumber(fc_tupdesc, fc_args[fc_i]);

		/* 坏家伙可能在 CREATE TRIGGER 中给我们提供不存在的列 */
		if (fc_fnumber <= 0)
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_COLUMN),
					 errmsg("there is no attribute \"%s\" in relation \"%s\"",
							fc_args[fc_i], SPI_getrelname(fc_rel))));

		/* 好吧，获取列的二进制（内部格式）值 */
		fc_kvals[fc_i] = SPI_getbinval(fc_trigtuple, fc_tupdesc, fc_fnumber, &fc_isnull);

		/*
		 * 如果它是 NULL，则无事可做！不要忘记调用 SPI_finish ()！
		 * 不要忘记返回元组！执行器插入您要返回的元组！
		 * 如果您返回 NULL，则不会插入任何内容！
		 */
		if (fc_isnull)
		{
			SPI_finish();
			return PointerGetDatum((fc_newtuple == NULL) ? fc_trigtuple : fc_newtuple);
		}

		/*
		 * 如果是 UPDATE，那么从要插入的新元组中获取列值并
		 * 比较这是否与旧值相同。目前我们使用值的字符串表示...
		 */
		if (fc_newtuple != NULL)
		{
			char	   *fc_oldval = SPI_getvalue(fc_trigtuple, fc_tupdesc, fc_fnumber);
			char	   *fc_newval;

			/* 这不应该发生！ SPI_ERROR_NOOUTFUNC ? */
			if (fc_oldval == NULL)
				/* 内部错误 */
				elog(ERROR, "check_foreign_key: SPI_getvalue returned %s", SPI_result_code_string(SPI_result));
			fc_newval = SPI_getvalue(fc_newtuple, fc_tupdesc, fc_fnumber);
			if (fc_newval == NULL || strcmp(fc_oldval, fc_newval) != 0)
				fc_isequal = false;
		}

		if (fc_plan->nplans <= 0)	/* 获取列的 typeId */
			fc_argtypes[fc_i] = SPI_gettypeid(fc_tupdesc, fc_fnumber);
	}
	fc_args_temp = fc_args;
	fc_nargs -= fc_nkeys;
	fc_args += fc_nkeys;

	/*
	 * 如果我们必须准备计划 ...
	 */
	if (fc_plan->nplans <= 0)
	{
		SPIPlanPtr	fc_pplan;
		char		fc_sql[8192];
		char	  **fc_args2 = fc_args;

		fc_plan->splan = (SPIPlanPtr *) MemoryContextAlloc(TopMemoryContext,
														fc_nrefs * sizeof(SPIPlanPtr));

		for (fc_r = 0; fc_r < fc_nrefs; fc_r++)
		{
			fc_relname = fc_args2[0];

			/*---------
			 * 对于 'R'estrict 操作，我们构造 SELECT 查询：
			 *
			 *	SELECT 1
			 *	FROM _referencing_relation_
			 *	WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]]
			 *
			 *	以检查元组是否被引用。
			 *---------
			 */
			if (fc_action == 'r')

				snprintf(fc_sql, sizeof(fc_sql), "select 1 from %s where ", fc_relname);

			/*---------
			 * 对于 'C'ascade 操作，我们构造 DELETE 查询
			 *
			 *	DELETE
			 *	FROM _referencing_relation_
			 *	WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]]
			 *
			 * 删除所有引用的元组。
			 *---------
			 */

			/*
			 * 最大：级联更新查询，我创建更新查询
			 * 在引用的表中更新新的键值
			 */


			else if (fc_action == 'c')
			{
				if (fc_is_update == 1)
				{
					int			fc_fn;
					char	   *fc_nv;
					int			fc_k;

					snprintf(fc_sql, sizeof(fc_sql), "update %s set ", fc_relname);
					for (fc_k = 1; fc_k <= fc_nkeys; fc_k++)
					{
						int			fc_is_char_type = 0;
						char	   *fc_type;

						fc_fn = SPI_fnumber(fc_tupdesc, fc_args_temp[fc_k - 1]);
						Assert(fc_fn > 0); /* 已在上面检查过 */
						fc_nv = SPI_getvalue(fc_newtuple, fc_tupdesc, fc_fn);
						fc_type = SPI_gettype(fc_tupdesc, fc_fn);

						if (strcmp(fc_type, "text") == 0 ||
							strcmp(fc_type, "varchar") == 0 ||
							strcmp(fc_type, "char") == 0 ||
							strcmp(fc_type, "bpchar") == 0 ||
							strcmp(fc_type, "date") == 0 ||
							strcmp(fc_type, "timestamp") == 0)
							fc_is_char_type = 1;
#ifdef	DEBUG_QUERY
						elog(DEBUG4, "check_foreign_key Debug value %s type %s %d",
							 fc_nv, fc_type, fc_is_char_type);
#endif

						/*
						 * is_char_type =1 我设置 ' ' 来定义一个新值
						 */
						snprintf(fc_sql + strlen(fc_sql), sizeof(fc_sql) - strlen(fc_sql),
								 " %s = %s%s%s %s ",
								 fc_args2[fc_k], (fc_is_char_type > 0) ? "'" : "",
								 fc_nv, (fc_is_char_type > 0) ? "'" : "", (fc_k < fc_nkeys) ? ", " : "");
					}
					strcat(fc_sql, " where ");
				}
				else
					/* 删除 */
					snprintf(fc_sql, sizeof(fc_sql), "delete from %s where ", fc_relname);
			}

			/*
			 * 对于 'S'etnull 操作，我们构造 UPDATE 查询 - UPDATE
			 * _referencing_relation_ SET Fkey1 null [, Fkey2 null [...]]
			 * WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]] - 将所有引用
			 * 元组中的键列设置为 NULL。
			 */
			else if (fc_action == 's')
			{
				snprintf(fc_sql, sizeof(fc_sql), "update %s set ", fc_relname);
				for (fc_i = 1; fc_i <= fc_nkeys; fc_i++)
				{
					snprintf(fc_sql + strlen(fc_sql), sizeof(fc_sql) - strlen(fc_sql),
							 "%s = null%s",
							 fc_args2[fc_i], (fc_i < fc_nkeys) ? ", " : "");
				}
				strcat(fc_sql, " where ");
			}

			/* 构造 WHERE 条件 */
			for (fc_i = 1; fc_i <= fc_nkeys; fc_i++)
			{
				snprintf(fc_sql + strlen(fc_sql), sizeof(fc_sql) - strlen(fc_sql), "%s = $%d %s",
						 fc_args2[fc_i], fc_i, (fc_i < fc_nkeys) ? "and " : "");
			}

			/* 为查询准备计划 */
			fc_pplan = SPI_prepare(fc_sql, fc_nkeys, fc_argtypes);
			if (fc_pplan == NULL)
				/* 内部错误 */
				elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result));

			/*
			 * 请记住，SPI_prepare 将计划放置在当前内存上下文
			 * - 因此，我们必须在顶部内存上下文中保存计划以供后用。
			 */
			if (SPI_keepplan(fc_pplan))
				/* 内部错误 */
				elog(ERROR, "check_foreign_key: SPI_keepplan failed");

			fc_plan->splan[fc_r] = fc_pplan;

			fc_args2 += fc_nkeys + 1; /* 到下一个关系 */
		}
		fc_plan->nplans = fc_nrefs;
#ifdef	DEBUG_QUERY
		elog(DEBUG4, "check_foreign_key Debug Query is :  %s ", fc_sql);
#endif
	}

	/*
	 * 如果 UPDATE 和键没有更改 ...
	 */
	if (fc_newtuple != NULL && fc_isequal)
	{
		SPI_finish();
		return PointerGetDatum(fc_newtuple);
	}

	/*
	 * 好吧，执行准备好的计划。
	 */
	for (fc_r = 0; fc_r < fc_nrefs; fc_r++)
	{
		/*
		 * 对于 'R'estrict，我们可能只执行一个元组的计划，对于其他
		 * 操作 - 对所有元组执行。
		 */
		int			fc_tcount = (fc_action == 'r') ? 1 : 0;

		fc_relname = fc_args[0];

		snprintf(fc_ident, sizeof(fc_ident), "%s$%u", fc_trigger->tgname, fc_rel->rd_id);
		fc_plan = fc_find_plan(fc_ident, &FPlans, &nFPlans);
		fc_ret = SPI_execp(fc_plan->splan[fc_r], fc_kvals, NULL, fc_tcount);
		/* 我们没有 NULL - 所以我们在这里传递   ^^^^ */

		if (fc_ret < 0)
			ereport(ERROR,
					(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
					 errmsg("SPI_execp returned %d", fc_ret)));

		/* 如果操作是 'R'estrict ... */
		if (fc_action == 'r')
		{
			/* 如果 SELECT 返回了元组，那么 ... */
			if (SPI_processed > 0)
				ereport(ERROR,
						(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
						 errmsg("\"%s\": tuple is referenced in \"%s\"",
								fc_trigger->tgname, fc_relname)));
		}
		else
		{
#ifdef REFINT_VERBOSE
			elog(NOTICE, "%s: " UINT64_FORMAT " tuple(s) of %s are %s",
				 fc_trigger->tgname, SPI_processed, fc_relname,
				 (fc_action == 'c') ? "deleted" : "set to null");
#endif
		}
		fc_args += fc_nkeys + 1;		/* 到下一个关系 */
	}

	SPI_finish();

	return PointerGetDatum((fc_newtuple == NULL) ? fc_trigtuple : fc_newtuple);
}

static EPlan *
fc_find_plan(char *fc_ident, EPlan **fc_eplan, int *fc_nplans)
{
	EPlan	   *fc_newp;
	int			fc_i;
	MemoryContext fc_oldcontext;

	/*
	 * 为计划完成的所有分配需要在会话安全的上下文中进行。
	 */
	fc_oldcontext = MemoryContextSwitchTo(TopMemoryContext);

	if (*fc_nplans > 0)
	{
		for (fc_i = 0; fc_i < *fc_nplans; fc_i++)
		{
			if (strcmp((*fc_eplan)[fc_i].ident, fc_ident) == 0)
				break;
		}
		if (fc_i != *fc_nplans)
		{
			MemoryContextSwitchTo(fc_oldcontext);
			return (*fc_eplan + fc_i);
		}
		*fc_eplan = (EPlan *) repalloc(*fc_eplan, (fc_i + 1) * sizeof(EPlan));
		fc_newp = *fc_eplan + fc_i;
	}
	else
	{
		fc_newp = *fc_eplan = (EPlan *) palloc(sizeof(EPlan));
		(*fc_nplans) = fc_i = 0;
	}

	fc_newp->ident = pstrdup(fc_ident);
	fc_newp->nplans = 0;
	fc_newp->splan = NULL;
	(*fc_nplans)++;

	MemoryContextSwitchTo(fc_oldcontext);
	return fc_newp;
}
